package com.zhs.type;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import com.zhs.javacc.SimpleNode;
import com.zhs.javacc.Token;
import com.zhs.tac.TACGeneration;
import com.zhs.xyzsdk.editors.XYZEditor;

public class TypeChecker {

	List<Variable> allVariables = new ArrayList<Variable>();

	public void checkAllVariableType(SimpleNode node) {
		// add all vars to a table...
		if (node.toString().equals("VarDeclaration")) {
			addVariables(node);
		} else if (node.toString().equals("MainClass")
				|| node.toString().equals("TypeDeclaration")) {
			addTypes(node);
		} else {
			for (int i = 0; i < node.jjtGetNumChildren(); i++) {
				checkAllVariableType((SimpleNode) node.jjtGetChild(i));
			}
		}
	}

	/**
	 * 
	 * @param node
	 *            node must be the root node of a parsing tree
	 */
	public void addTypes(SimpleNode parent) {
		Type.reset();

		if (parent == null) {
			return;
		}

		for (int k = 0; k < parent.jjtGetNumChildren(); k++) {
			SimpleNode node = (SimpleNode) parent.jjtGetChild(k);
			Type type = new Type();

			if (node.toString().equals("MainClass")) {

				// set the type name
				type.setTypeName(((SimpleNode) node.jjtGetChild(0))
						.jjtGetFirstToken().image);

				Variable var = new Variable();
				// the "args" is the second child of the mainclass node
				var.setVarName(((SimpleNode) (node.jjtGetChild(1)))
						.jjtGetFirstToken().image);
				// and it must be of the type String[]
				var.setTypeName("String[]");
				// but this is in the method scope. A very rare type, since it
				// can
				// only be found here. By the gramma, there should only be int
				// and
				// double arrays across the code. String[] is read-only because
				// there is no way we can manipulate the data inside.

				Method mainMethod = new Method();
				mainMethod.setMethodName("main");
				mainMethod.addParameter(var);
				mainMethod.setHostTypeName(type.getTypeName());
				// good habit - even pointless here
				mainMethod.getScope().setParent(type.getScope());
				mainMethod.addStatementNode((SimpleNode) node.jjtGetChild(2));
				type.addMethod(mainMethod);

				// now the statements
				addStatements(mainMethod, node, 2);

			} else {

				// set the type name
				type.setTypeName(((SimpleNode) node.jjtGetChild(0).jjtGetChild(
						0)).jjtGetFirstToken().image);

				// The first child is ClassDec or ClassExtendsDec
				SimpleNode child = (SimpleNode) node.jjtGetChild(0);
				if (child.toString().equals("ClassDeclaration")) {
					// varDec first, then methods.
					addTypeMembers(type, child, 1);
				} else if (child.toString().equals("ClassExtendsDeclaration")) {
					type.setSuperType(((SimpleNode) (child.jjtGetChild(1)))
							.jjtGetFirstToken().image);
					// varDec first, then methods.
					addTypeMembers(type, child, 2);
				}
			}
			if (!Type.registerType(type)) {
				XYZEditor.addMarker(
						"Duplicate classes: found at least two classes named: "
								+ type.getTypeName(),
						node.jjtGetFirstToken().beginLine);
			}
		}
		verify();
		new TACGeneration();
	}

	private void verify() {
		Collection<Type> types = Type.getAllTypes();
		for (Type t : types) {
			Collection<Method> methods = t.getExclusiveMethods();
			for (Method m : methods) {
				for (SimpleNode parent : m.getStatementNodes()) {
					// verify the statements
					SimpleNode node = (SimpleNode) parent.jjtGetChild(0);
					// must have one child and is the of the sub-type of
					// statement
					testStatement(m.getScope(), node, t);
				}
				checkReturnExpression(m, m.getReturnNode());
			}
		}
	}

	private void testStatement(Scope scope, SimpleNode node, Type type) {
		if (node.toString().equals("Statement")) {
			testStatement(scope, (SimpleNode) node.jjtGetChild(0), type);
		} else if (node.toString().equals("Block")) {
			for (int i = 0; i < node.jjtGetNumChildren(); i++) {
				testStatement(scope, ((SimpleNode) node.jjtGetChild(i)), type);
			}
		} else if (node.toString().equals("IfStatement")) {
			// if (exp) statement else statement
			Expression exp = new Expression();
			exp.setScope(scope);
			exp.setValue((SimpleNode) node.jjtGetChild(0));
			exp.setType(type);
			String expType = exp.visit();
			if (expType.equals("boolean")) {
				testStatement(scope, (SimpleNode) node.jjtGetChild(1), type);
				testStatement(scope, (SimpleNode) node.jjtGetChild(2), type);
			} else {
				XYZEditor.addMarker(
						"Type mismatch, expecting boolean type in if statement"
								+ ", found:" + expType, ((SimpleNode) node
								.jjtGetChild(0)).jjtGetFirstToken().beginLine);
			}
		} else if (node.toString().equals("WhileStatement")) {
			// while (exp) statement
			Expression exp = new Expression();
			exp.setScope(scope);
			exp.setValue((SimpleNode) node.jjtGetChild(0));
			exp.setType(type);
			String expType = exp.visit();
			if (expType.equals("boolean")) {
				testStatement(scope, (SimpleNode) node.jjtGetChild(1), type);
			} else {
				XYZEditor.addMarker(
						"Type mismatch, expecting boolean type in while statement"
								+ ", found:" + expType, ((SimpleNode) node
								.jjtGetChild(0)).jjtGetFirstToken().beginLine);
			}
		} else if (node.toString().equals("DoWhileStatement")) {
			// do statement while(exp);
			Expression exp = new Expression();
			exp.setScope(scope);
			exp.setValue((SimpleNode) node.jjtGetChild(1));
			exp.setType(type);
			String expType = exp.visit();
			if (expType.equals("boolean")) {
				testStatement(scope, (SimpleNode) node.jjtGetChild(0), type);
			} else {
				XYZEditor.addMarker(
						"Type mismatch, expecting boolean type in while statement"
								+ ", found:" + expType, ((SimpleNode) node
								.jjtGetChild(1)).jjtGetFirstToken().beginLine);
			}
		} else if (node.toString().equals("AssignmentStatement")) {
			// id = exp;
			Variable var = scope.lookupVariable(((SimpleNode) node
					.jjtGetChild(0)).jjtGetFirstToken().image);
			if (var == null) {
				// no id found in the scope
				XYZEditor
						.addMarker("Error, identifier undeclared in scope:"
								+ ((SimpleNode) node.jjtGetChild(0))
										.jjtGetFirstToken().image,
								((SimpleNode) node.jjtGetChild(0))
										.jjtGetFirstToken().beginLine);
			} else {
				// type match
				Expression exp = new Expression();
				exp.setScope(scope);
				exp.setValue((SimpleNode) node.jjtGetChild(1));
				exp.setType(type);
				String typeName = exp.visit();
				if (typePromotion(var.getTypeName(), typeName).length() > 0) {
					// matches
				} else {
					XYZEditor.addMarker(
							"Type mismatch, incompatible types for assignment: "
									+ var.getTypeName() + " vs " + typeName,
							((SimpleNode) node.jjtGetChild(1))
									.jjtGetFirstToken().beginLine);
				}
			}
		} else if (node.toString().equals("ArrayAssignmentStatement")) {
			// id[exp] = exp;
			Variable var = scope.lookupVariable(((SimpleNode) node
					.jjtGetChild(0)).jjtGetFirstToken().image);
			if (var == null) {
				XYZEditor
						.addMarker("Error, identifier undeclared in scope:"
								+ ((SimpleNode) node.jjtGetChild(0))
										.jjtGetFirstToken().image,
								((SimpleNode) node.jjtGetChild(0))
										.jjtGetFirstToken().beginLine);
			} else {
				// type match
				Expression offsetExp = new Expression();
				offsetExp.setScope(scope);
				offsetExp.setValue((SimpleNode) node.jjtGetChild(1));
				offsetExp.setType(type);
				String typeName = offsetExp.visit();
				if (typePromotion("int", typeName).length() > 0) {
					// offset must be int
					Expression exp = new Expression();
					exp.setScope(scope);
					exp.setValue((SimpleNode) node.jjtGetChild(2));
					exp.setType(type);
					String expType = exp.visit();
					if (var.getTypeName().equals("int[]")) {
						if (typePromotion("int", expType).length() > 0) {
							// ok
						} else {
							XYZEditor.addMarker(
									"Type mismatch: not assigning integers to"
											+ " integer array element, found"
											+ expType,
									((SimpleNode) node.jjtGetChild(2))
											.jjtGetFirstToken().beginLine);
						}
					} else if (var.getTypeName().equals("double[]")) {
						if (typePromotion("double", exp.visit()).length() > 0) {
							// ok
						} else {
							XYZEditor.addMarker(
									"Type mismatch: not assigning doubles to"
											+ " double array element, found"
											+ expType,
									((SimpleNode) node.jjtGetChild(2))
											.jjtGetFirstToken().beginLine);
						}
					} else {
						XYZEditor.addMarker(
								"Type mismatch: not an array type for indexing:"
										+ ", found: " + expType,
								((SimpleNode) node.jjtGetChild(0))
										.jjtGetFirstToken().beginLine);
					}
				} else {
					XYZEditor.addMarker(
							"Type mismatch: array offset is not int, found: "
									+ typeName,
							((SimpleNode) node.jjtGetChild(1))
									.jjtGetFirstToken().beginLine);
				}
			}
		} else if (node.toString().equals("PrintStatement")) {
			// sout(exp);
			// seems ok to accept any parameter....
			Expression exp = new Expression();
			exp.setScope(scope);
			exp.setValue((SimpleNode) node.jjtGetChild(0));
			exp.setType(type);
			exp.visit();
		}
	}

	/**
	 * Add variable and method declaration to a type
	 * 
	 * @param type
	 *            the type
	 * @param parent
	 *            node of the type
	 * @param i
	 *            offset of first variable node(maybe the first statement or
	 *            even out of bounds if the type contains nothing)
	 */
	private void addTypeMembers(Type type, SimpleNode parent, int i) {
		for (; i < parent.jjtGetNumChildren(); i++) {
			SimpleNode child = (SimpleNode) parent.jjtGetChild(i);
			if (child.toString().equals("MethodDeclaration")) {
				addMethods(type, parent, i);
				return;
			}
			if (child.toString().equals("VarDeclaration")) {
				Variable var = new Variable();
				String varType = getTypeNameFromNode((SimpleNode) child
						.jjtGetChild(0));
				var.setVarName(((SimpleNode) child.jjtGetChild(1))
						.jjtGetFirstToken().image);
				var.setTypeName(varType);
				if (type.getScope().addVariable(var) != null) {
					XYZEditor.addMarker("Duplicate fields, found at least "
							+ "two variables with the name: "
							+ var.getVarName(), ((SimpleNode) child
							.jjtGetChild(1)).jjtGetFirstToken().beginLine);
				}
			}

		}
	}

	/**
	 * Gets the type of a type node
	 * 
	 * @param node
	 * @return the name of the type e.g. "int[]" for IntArrayType
	 */
	private String getTypeNameFromNode(SimpleNode node) {
		String varType = node.toString();
		if (varType.equals("IntArrayType")) {
			return "int[]";
		} else if (varType.equals("BooleanType")) {
			return ("boolean");
		} else if (varType.equals("Identifier")) {
			return node.jjtGetFirstToken().image;
		} else if (varType.equals("IntegerType")) {
			return ("int");
		} else if (varType.equals("DoubleArrayType")) {
			return ("double[]");
		} else if (varType.equals("DoubleType")) {
			return ("double");
		} else
			return "";
	}

	/**
	 * Add method declaration to a type
	 * 
	 * @param type
	 *            the type
	 * @param parent
	 *            node of the type
	 * @param i
	 *            offset of first method node(maybe out of bounds if the type
	 *            contains nothing)
	 */
	private void addMethods(Type type, SimpleNode parent, int i) {
		for (; i < parent.jjtGetNumChildren(); i++) {
			SimpleNode node = (SimpleNode) parent.jjtGetChild(i);
			Method method = new Method();

			method.setHostTypeName(type.getTypeName());

			String returnTypeName = getTypeNameFromNode((SimpleNode) node
					.jjtGetChild(0));
			method.setReturnTypeName(returnTypeName);

			String methodName = ((SimpleNode) node.jjtGetChild(1))
					.jjtGetFirstToken().image;
			method.setMethodName(methodName);

			Scope scope = new Scope();
			scope.setParent(type.getScope());
			method.setScope(scope);

			// now the parameters
			if (node.jjtGetChild(2).toString().equals("FormalParameterList")) {
				SimpleNode parameterList = (SimpleNode) node.jjtGetChild(2);
				for (int j = 0; j < parameterList.jjtGetNumChildren(); j++) {
					SimpleNode parameterNode = (SimpleNode) parameterList
							.jjtGetChild(j);
					Variable var = new Variable();
					var.setVarName(((SimpleNode) parameterNode.jjtGetChild(1))
							.jjtGetFirstToken().image);
					String parameterType = getTypeNameFromNode((SimpleNode) parameterNode
							.jjtGetChild(0));
					var.setTypeName(parameterType);
					if (method.addParameter(var) != null) {
						// duplicate parameter
						XYZEditor.addMarker(
								"Duplicate parameters, found at least two parameters "
										+ "with the name: " + var.getVarName(),
								parameterNode.jjtGetFirstToken().beginLine);
					}
				}
				addMethodMembers(method, node, 3);
			} else {
				addMethodMembers(method, node, 2);
			}

			if (type.addMethod(method) != null) {
				// already exists a method with the same name.
				XYZEditor.addMarker(
						"Duplicate methods, found at least two methods with the name: "
								+ method.getMethodName(), ((SimpleNode) node
								.jjtGetChild(1)).jjtGetFirstToken().beginLine);
			}

		}

		// check duplicate methods after all finished.

	}

	private void addMethodMembers(Method method, SimpleNode parent, int i) {
		for (; i < parent.jjtGetNumChildren(); i++) {
			SimpleNode node = (SimpleNode) parent.jjtGetChild(i);
			if (node.toString().equals("Statement")) {
				addStatements(method, parent, i);
				if (parent.jjtGetChild(parent.jjtGetNumChildren() - 1)
						.toString().equals("Return")) {
					method.setReturnNode((SimpleNode) parent.jjtGetChild(parent
							.jjtGetNumChildren() - 1));
				}
				return;
			}
			if (node.toString().equals("Return")) {
				method.setReturnNode(node);
			}
			if (node.toString().equals("VarDeclaration")) {
				Variable var = new Variable();
				String varType = getTypeNameFromNode((SimpleNode) node
						.jjtGetChild(0));
				var.setVarName(((SimpleNode) node.jjtGetChild(1))
						.jjtGetFirstToken().image);
				var.setTypeName(varType);

				if (method.addVariableToScope(var) != null) {
					// duplicate variable name
					XYZEditor.addMarker(
							"Duplicate variable declaration, found at least"
									+ " two vaiables with the name: "
									+ var.getVarName(),
							((SimpleNode) node.jjtGetChild(1))
									.jjtGetFirstToken().beginLine);
				}
			}
		}
	}

	private void checkReturnExpression(Method method, SimpleNode node) {
		if (node == null)
			return;
		if (node.toString().equals("Return")) {
			Expression exp = new Expression();
			exp.setScope(method.getScope());
			exp.setValue((SimpleNode) node.jjtGetChild(0));
			exp.setType(Type.lookupType(method.getHostTypeName()));
			String type = exp.visit();
			if (typePromotion(method.getReturnTypeName(), type).length() > 0) {
				// return type matches
			} else {
				if (type.length() > 0) {
					XYZEditor.addMarker("Return type mismatch," + "expecting: "
							+ method.getReturnTypeName() + ", found: " + type,
							node.jjtGetLastToken().beginLine);
				} else {
					XYZEditor.addMarker("Undefined type: "
							+ ((SimpleNode) node.jjtGetChild(0))
									.jjtGetFirstToken().image, node
							.jjtGetLastToken().beginLine);
				}
			}

		} else {
			// TODO issue an error
			// no return phrase... should not be possible if filtered
			// through
			// grammar.
		}
	}

	/**
	 * Adds the statements to a method
	 * 
	 * @param method
	 *            the Method
	 * @param parent
	 *            parent node of the parallel statements - a method node
	 * @param i
	 *            offset of the first statement in the node
	 */
	private void addStatements(Method method, SimpleNode parent, int i) {
		for (; i < parent.jjtGetNumChildren() - 1; i++) {
			SimpleNode node = (SimpleNode) parent.jjtGetChild(i);
			Statement statement = new Statement();
			statement.getScope().setParent(method.getScope());
			// from statement to expressions...hard parts start here
			// the statements have the method scope, they can see variables
			// declared in the method as well as in the class.

			// methods can be recursively self-containing
			method.addStatementNode(node);
		}
	}

	private void addVariables(SimpleNode node) {
		// it is a var declaration, and must have 2 children.
		// only arrays have more than 2 tokens.
		Variable var = new Variable();
		Token last = node.jjtGetLastToken();
		SimpleNode type = (SimpleNode) node.jjtGetChild(0);
		if (type.toString().equals("intArrayType")) {
			var.setTypeName("int[]");
		} else if (type.toString().equals("doubleArrayType")) {
			var.setTypeName("double[]");
		} else {
			var.setTypeName(type.jjtGetFirstToken().image);
		}
		var.setVarName(last.image);
		allVariables.add(var);
	}

	private String typePromotion(String host, String guest) {
		if (host.equals(guest)) {
			return host;
		} else if (host.equals("double")) {
			if (guest.equals("int"))
				return host;
		} else {
			Type hostType = Type.lookupType(host);
			Type guestType = Type.lookupType(guest);
			if (hostType == null) {
				// TODO
				return "";
			}
			if (guestType == null) {
				// TODO
				return "";
			}
			String temp = guestType.getSuperType();
			while (temp != null) {
				if (temp.equals(host)) {
					return host;
				}
				Type tempType = Type.lookupType(temp);
				if (tempType == null) {
					// TODO
					return "";
				}
				temp = tempType.getSuperType();
			}
		}
		// TODO
		return "";
	}
}
